package gateway

import (
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"github.com/hyperledger/fabric/bccsp"
	"github.com/hyperledger/fabric/bccsp/factory"
	"github.com/pkg/errors"
	"io/ioutil"
	"os"
	"path/filepath"
)

func DefaultFactoryOpt() *factory.FactoryOpts {
	return &factory.FactoryOpts{
		ProviderName: "SW",
		SwOpts: &factory.SwOpts{
			SecLevel:   256,
			HashFamily: "SHA2",
			//Ephemeral:true,
		},
	}
}

type CryptoSuite interface {
	NewSigner() (Signer, error)
	GetCreator() ([]byte, error)
}

type cryptoSuite struct {
	cert       *x509.Certificate
	mspId      string
	factoryOpt *factory.FactoryOpts
}

func newCryptoSuiteFromLocal(dir, mspid string) (*cryptoSuite, error) {
	certDir := filepath.Join(dir, "signcerts")
	keyDir := filepath.Join(dir, "keystore")
	certBytes, err := getPemMaterialFromDir(certDir)
	if err != nil || len(certBytes) == 0 {
		return nil, fmt.Errorf("get certfile fail:%v, certfile:%s", err, certDir)
	}
	cert, err := getCertFromPem(certBytes[0])
	if err != nil {
		return nil, err
	}
	cryptoSuite := &cryptoSuite{
		cert:       cert,
		mspId:      mspid,
		factoryOpt: DefaultFactoryOpt(),
	}
	cryptoSuite.factoryOpt.SwOpts.FileKeystore = &factory.FileKeystoreOpts{KeyStorePath: keyDir}
	return cryptoSuite, nil
}

func (suite cryptoSuite) newbccsp() (bccsp.BCCSP, error) {
	f := &factory.SWFactory{}
	swbccsp, err := f.Get(suite.factoryOpt)
	if err != nil {
		return nil, err
	}
	return swbccsp, nil
}

func (suite *cryptoSuite) NewSigner() (Signer, error) {
	bccspInst, err := suite.newbccsp()
	if err != nil {
		return nil, err
	}
	prk, err := bccspInst.KeyImport(suite.cert, &bccsp.X509PublicKeyImportOpts{Temporary: true})
	if err != nil {
		return nil, err
	}
	return &localSigner{
		bccsp:     bccspInst,
		cert:      suite.cert,
		prik:      prk,
		cryptoOpt: suite.factoryOpt,
		mspid:     suite.mspId,
	}, nil
}

func (suite *cryptoSuite) GetCreator() ([]byte, error) {
	signer, err := suite.NewSigner()
	if err != nil {
		return nil, err
	}
	return signer.Serialize()
}

func getPemMaterialFromDir(dir string) ([][]byte, error) {
	_, err := os.Stat(dir)
	if os.IsNotExist(err) {
		return nil, err
	}

	content := make([][]byte, 0)
	files, err := ioutil.ReadDir(dir)
	if err != nil {
		return nil, errors.Wrapf(err, "could not read directory %s", dir)
	}

	for _, f := range files {
		fullName := filepath.Join(dir, f.Name())

		f, err := os.Stat(fullName)
		if err != nil {
			fmt.Printf("Failed to stat %s: %s\n", fullName, err)
			continue
		}
		if f.IsDir() {
			continue
		}

		item, err := readPemFile(fullName)
		if err != nil {
			fmt.Printf("Failed reading file %s: %s\n", fullName, err)
			continue
		}

		content = append(content, item)
	}

	return content, nil
}

func readPemFile(file string) ([]byte, error) {
	bytes, err := ioutil.ReadFile(file)
	if err != nil {
		return nil, errors.Wrapf(err, "reading from file %s failed", file)
	}

	b, _ := pem.Decode(bytes)
	if b == nil { // TODO: also check that the type is what we expect (cert vs key..)
		return nil, errors.Errorf("no pem content for file %s", file)
	}

	return bytes, nil
}

func getCertFromPem(idBytes []byte) (*x509.Certificate, error) {
	if idBytes == nil {
		return nil, errors.New("getCertFromPem error: nil idBytes")
	}

	// Decode the pem bytes
	pemCert, _ := pem.Decode(idBytes)
	if pemCert == nil {
		return nil, errors.Errorf("getCertFromPem error: could not decode pem bytes [%v]", idBytes)
	}

	// get a cert
	var cert *x509.Certificate
	cert, err := x509.ParseCertificate(pemCert.Bytes)
	if err != nil {
		return nil, errors.Wrap(err, "getCertFromPem error: failed to parse x509 cert")
	}

	return cert, nil
}
